<template>
  <van-dialog v-model:show="_show" :title="title" confirm-button-text="关闭" @confirm="emit('cancel')">
    <slot></slot>
    <qrcode-stream
      :constraints="selectedConstraints"
      :formats="selectedBarcodeFormats"
      :track="trackFunctionSelected.value"
      style="left: 50%; transform: translateX(-50%)"
      @detect="onDetect"
      @error="onError"
      @camera-on="onCameraReady"
    />
  </van-dialog>
</template>

<script lang="ts" setup>
import { ref, computed, Ref } from 'vue'
import { QrcodeStream } from 'vue-qrcode-reader'

const props = withDefaults(
  defineProps<{
    title?: string
    show: boolean
  }>(),
  {
    title: ''
  }
)
const { show } = toRefs(props)
const emit = defineEmits<{
  (e: 'update:show', value: boolean): void
  (e: 'onCaptured', value: any): void
  (e: 'cancel'): void
}>()

/*** detection handling ***/

const result = ref('')

function onDetect(detectedCodes: any) {
  result.value = detectedCodes.map((code: any) => code.rawValue)[0]
  console.log('onDetect', result.value, detectedCodes)
  emit('onCaptured', result.value)
}

/*** select camera ***/

const selectedConstraints = ref({ facingMode: 'environment' })
const defaultConstraintOptions = [
  { label: 'rear camera', constraints: { facingMode: 'environment' } },
  { label: 'front camera', constraints: { facingMode: 'user' } }
]
const constraintOptions: Ref<any> = ref(defaultConstraintOptions)

const onCameraReady = async () => {
  // NOTE: on iOS we can't invoke `enumerateDevices` before the user has given
  // camera access permission. `QrcodeStream` internally takes care of
  // requesting the permissions. The `camera-on` event should guarantee that this
  // has happened.
  const devices = await navigator.mediaDevices.enumerateDevices()
  const videoDevices = devices.filter(({ kind }) => kind === 'videoinput')

  constraintOptions.value = [
    ...defaultConstraintOptions,
    ...videoDevices.map(({ deviceId, label }) => ({
      label: `${label} (ID: ${deviceId})`,
      constraints: { deviceId }
    }))
  ]

  error.value = ''
}

/*** track functons ***/

const paintOutline = (detectedCodes: any, ctx: any) => {
  for (const detectedCode of detectedCodes) {
    const [firstPoint, ...otherPoints] = detectedCode.cornerPoints

    ctx.strokeStyle = 'red'

    ctx.beginPath()
    ctx.moveTo(firstPoint.x, firstPoint.y)
    for (const { x, y } of otherPoints) {
      ctx.lineTo(x, y)
    }
    ctx.lineTo(firstPoint.x, firstPoint.y)
    ctx.closePath()
    ctx.stroke()
  }
}
const paintBoundingBox = (detectedCodes: any, ctx: any) => {
  for (const detectedCode of detectedCodes) {
    const {
      boundingBox: { x, y, width, height }
    } = detectedCode

    ctx.lineWidth = 2
    ctx.strokeStyle = '#007bff'
    ctx.strokeRect(x, y, width, height)
  }
}
const paintCenterText = (detectedCodes: any, ctx: any) => {
  for (const detectedCode of detectedCodes) {
    const { boundingBox, rawValue } = detectedCode

    const centerX = boundingBox.x + boundingBox.width / 2
    const centerY = boundingBox.y + boundingBox.height / 2

    const fontSize = Math.max(12, (50 * boundingBox.width) / ctx.canvas.width)

    ctx.font = `bold ${fontSize}px sans-serif`
    ctx.textAlign = 'center'

    ctx.lineWidth = 3
    ctx.strokeStyle = '#35495e'
    ctx.strokeText(detectedCode.rawValue, centerX, centerY)

    ctx.fillStyle = '#5cb984'
    ctx.fillText(rawValue, centerX, centerY)
  }
}
const trackFunctionOptions = [
  { text: 'nothing (default)', value: undefined },
  { text: 'outline', value: paintOutline },
  { text: 'centered text', value: paintCenterText },
  { text: 'bounding box', value: paintBoundingBox }
]
const trackFunctionSelected = ref(trackFunctionOptions[1])

/*** barcode formats ***/

const barcodeFormats: Ref<any> = ref({
  aztec: false,
  code_128: false,
  code_39: false,
  code_93: false,
  codabar: false,
  databar: false,
  databar_expanded: false,
  data_matrix: false,
  dx_film_edge: false,
  ean_13: false,
  ean_8: false,
  itf: false,
  maxi_code: false,
  micro_qr_code: true,
  pdf417: false,
  qr_code: true,
  rm_qr_code: true,
  upc_a: false,
  upc_e: false,
  linear_codes: false,
  matrix_codes: false
})
const selectedBarcodeFormats = computed(() => {
  return Object.keys(barcodeFormats.value).filter((format) => barcodeFormats.value[format])
})

/*** error handling ***/

const error = ref('')

const onError = (err: any) => {
  error.value = `[${err.name}]: `

  if (err.name === 'NotAllowedError') {
    error.value += 'you need to grant camera access permission'
  } else if (err.name === 'NotFoundError') {
    error.value += 'no camera on this device'
  } else if (err.name === 'NotSupportedError') {
    error.value += 'secure context required (HTTPS, localhost)'
  } else if (err.name === 'NotReadableError') {
    error.value += 'is the camera already in use?'
  } else if (err.name === 'OverconstrainedError') {
    error.value += 'installed cameras are not suitable'
  } else if (err.name === 'StreamApiNotSupportedError') {
    error.value += 'Stream API is not supported in this browser'
  } else if (err.name === 'InsecureContextError') {
    error.value += 'Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
  } else {
    error.value += err.message
  }
}

const _show = ref(props.show)
watch(
  () => _show.value,
  (newValue, oldValue) => {
    if (oldValue !== newValue) {
      emit('update:show', _show.value)
    }
  }
)
watch(
  () => show.value,
  (newValue, oldValue) => {
    if (oldValue !== newValue) {
      _show.value = show.value
    }
  }
)
</script>

<style lang="less" scoped>
.container {
  height: 60vh;
}

#reader {
  top: 50%;
  left: 0;
  transform: translateY(-50%);
}
</style>
